import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from plotly.figure_factory import create_distplot
import seaborn as sns
from random import choices
from scipy.stats import ttest_ind, shapiro
%matplotlib inline
C:\Users\kpaqk\anaconda3\lib\site-packages\numpy\_distributor_init.py:30: UserWarning: loaded more than 1 DLL from .libs:
C:\Users\kpaqk\anaconda3\lib\site-packages\numpy\.libs\libopenblas.WCDJNK7YVMPZQ2ME2ZZHJJRJ3JIKNDB7.gfortran-win_amd64.dll
C:\Users\kpaqk\anaconda3\lib\site-packages\numpy\.libs\libopenblas.XWYDX2IKJW2NMTWSFYNGFUWKQU3LYTCZ.gfortran-win_amd64.dll
warnings.warn("loaded more than 1 DLL from .libs:"
МТС Библиотека - приложение для чтения электронных книг, прессы и прослушивания аудиокниг, доступно для абонентов всех мобильных операторов, продукт экосистемы МТС.
В представленном датасете собраны данные по пользователям и книгам, а также по их взаимодействиям (прочтение книги пользователем) из сервиса МТС Библиотека. Данные по чтению пользователями книг собраны за 2 два года, с 01-01-2018 по 31-12-2019 включительно, и разбавлены случайным шумом. ID пользователей и книг анонимизированы.
Статистика по датасету:
В данном файле содержится информация о пользователях:
данный признак - результат работы модели
- 18_24 - от 18 до 24 лет включительно
- 25_34 - от 25 до 34 лет включительно
- 35_44 - от 35 до 44 лет включительно
- 45_54 - от 45 до 54 лет включительно
- 55_64 - от 55 до 64 лет включительно
- 65_inf - от 65 и старше
- NaN - неизвестно
данный признак - результат работы модели
- 1 - мужчина
- 0 - женщина
- NaN - неизвестно
В данном файле содержится информация о книгах:
В данном файле содержится информация о взаимодействиях пользователей и книг:
df = pd.read_csv('interactions.csv')
df_users = pd.read_csv('users.csv')
df_items = pd.read_csv('items.csv')
df.head()
| user_id | item_id | progress | rating | start_date | |
|---|---|---|---|---|---|
| 0 | 126706 | 14433 | 80 | NaN | 2018-01-01 |
| 1 | 127290 | 140952 | 58 | NaN | 2018-01-01 |
| 2 | 66991 | 198453 | 89 | NaN | 2018-01-01 |
| 3 | 46791 | 83486 | 23 | 5.0 | 2018-01-01 |
| 4 | 79313 | 188770 | 88 | 5.0 | 2018-01-01 |
df.shape
(1533078, 5)
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1533078 entries, 0 to 1533077 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 1533078 non-null int64 1 item_id 1533078 non-null int64 2 progress 1533078 non-null int64 3 rating 285356 non-null float64 4 start_date 1533078 non-null object dtypes: float64(1), int64(3), object(1) memory usage: 58.5+ MB
Так как очень много пропущенных значений в rating, удалю NaNы, чтобы упростить дальнейшую проверку гипотез.
df.dropna(inplace=True)
df['start_date'] = pd.to_datetime(df['start_date'])
df.reset_index(drop=True, inplace=True)
df.head()
| user_id | item_id | progress | rating | start_date | |
|---|---|---|---|---|---|
| 0 | 46791 | 83486 | 23 | 5.0 | 2018-01-01 |
| 1 | 79313 | 188770 | 88 | 5.0 | 2018-01-01 |
| 2 | 42797 | 315927 | 69 | 5.0 | 2018-01-01 |
| 3 | 23439 | 9762 | 74 | 4.0 | 2018-01-01 |
| 4 | 115029 | 34757 | 1 | 4.0 | 2018-01-01 |
df.shape
(285356, 5)
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 285356 entries, 0 to 285355 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 285356 non-null int64 1 item_id 285356 non-null int64 2 progress 285356 non-null int64 3 rating 285356 non-null float64 4 start_date 285356 non-null datetime64[ns] dtypes: datetime64[ns](1), float64(1), int64(3) memory usage: 10.9 MB
plt.title('Гистограмма частот для progress')
sns.histplot(df['progress'], bins=10)
plt.show()
num = df['progress'].count()
num_full = (df['progress'] == 100).sum()
print(f"Всего пользователи прочитали {num} книг")
print(f"Из них прочитали до конца {num_full} ~ {num_full/num*100:.2f}%")
print(f"Из них не дочитали {num - num_full} ~ {(num - num_full)/num*100:.2f}%")
Всего пользователи прочитали 285356 книг Из них прочитали до конца 111544 ~ 39.09% Из них не дочитали 173812 ~ 60.91%
plt.title('Гистограмма частот для rating')
sns.histplot(df['rating'], bins=10)
plt.show()
rating = df['rating'].count()
for i in range(5, 0, -1):
print(f"{(df['rating']==i).sum()/rating*100:.2f}% пользователей оценили книгу {i}" )
75.24% пользователей оценили книгу 5 14.15% пользователей оценили книгу 4 6.40% пользователей оценили книгу 3 2.05% пользователей оценили книгу 2 2.14% пользователей оценили книгу 1
plt.title('Гистограмма частот для start_date')
sns.histplot(df['start_date'])
plt.show()
plt.title('Гистограмма частот для item_id')
sns.histplot(df['item_id'])
plt.show()
plt.title('Гистограмма частот для user_id')
sns.histplot(df['user_id'])
plt.show()
df_users.head()
| user_id | age | sex | |
|---|---|---|---|
| 0 | 1 | 45_54 | NaN |
| 1 | 2 | 18_24 | 0.0 |
| 2 | 3 | 65_inf | 0.0 |
| 3 | 4 | 18_24 | 0.0 |
| 4 | 5 | 35_44 | 0.0 |
df_users.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 142888 entries, 0 to 142887 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 142888 non-null int64 1 age 142742 non-null object 2 sex 136626 non-null float64 dtypes: float64(1), int64(1), object(1) memory usage: 3.3+ MB
df_users.dropna(inplace=True)
plt.title('Гистограмма частот для age')
sns.histplot(df_users['age'])
plt.show()
age_all = df_users['age'].count()
for age in sorted(df_users['age'].unique()):
print(f"Всего {(df_users['age']==age).sum()/age_all*100:.2f}%",
f"пользователей в возрасте от {' до '.join(age.replace('inf', '100').split('_'))} лет")
Всего 39.16% пользователей в возрасте от 18 до 24 лет Всего 20.85% пользователей в возрасте от 25 до 34 лет Всего 12.12% пользователей в возрасте от 35 до 44 лет Всего 9.94% пользователей в возрасте от 45 до 54 лет Всего 11.57% пользователей в возрасте от 55 до 64 лет Всего 6.35% пользователей в возрасте от 65 до 100 лет
plt.title('Гистограмма частот для пола')
sns.histplot(df_users['sex'], bins=10)
plt.show()
(df_users['sex'] == 0).sum()
91897
print(f"{(df_users['sex'] == 0).sum()/df_users['sex'].count()*100:.2f}% пользователей женского пола")
print(f"{(df_users['sex'] == 1).sum()/df_users['sex'].count()*100:.2f}% пользователей мужского пола")
67.33% пользователей женского пола 32.67% пользователей мужского пола
for age in sorted(df_users['age'].unique()):
df_age = df_users[df_users['age'] == age]
print(f"■ Среди пользователей от {' до '.join(age.replace('inf', '100').split('_'))} лет ")
print(f"{(df_age['sex'] == 0).sum()/df_age['sex'].count()*100:.2f}% пользователей женского пола")
print(f"{(df_age['sex'] == 1).sum()/df_age['sex'].count()*100:.2f}% пользователей мужского пола\n")
■ Среди пользователей от 18 до 24 лет 68.43% пользователей женского пола 31.57% пользователей мужского пола ■ Среди пользователей от 25 до 34 лет 68.26% пользователей женского пола 31.74% пользователей мужского пола ■ Среди пользователей от 35 до 44 лет 67.28% пользователей женского пола 32.72% пользователей мужского пола ■ Среди пользователей от 45 до 54 лет 65.77% пользователей женского пола 34.23% пользователей мужского пола ■ Среди пользователей от 55 до 64 лет 65.20% пользователей женского пола 34.80% пользователей мужского пола ■ Среди пользователей от 65 до 100 лет 63.97% пользователей женского пола 36.03% пользователей мужского пола
df_items.head()
| id | title | genres | authors | year | |
|---|---|---|---|---|---|
| 0 | 128115 | Ворон-челобитчик | Зарубежные детские книги,Сказки,Зарубежная кла... | Михаил Салтыков-Щедрин | 1886 |
| 1 | 210979 | Скрипка Ротшильда | Классическая проза,Литература 19 века,Русская ... | Антон Чехов | 1894 |
| 2 | 95632 | Испорченные дети | Зарубежная классика,Классическая проза,Литерат... | Михаил Салтыков-Щедрин | 1869 |
| 3 | 247906 | Странный человек | Пьесы и драматургия,Литература 19 века | Михаил Лермонтов | 1831 |
| 4 | 294280 | Господа ташкентцы | Зарубежная классика,Классическая проза,Литерат... | Михаил Салтыков-Щедрин | 1873 |
df_items.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 59599 entries, 0 to 59598 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 59599 non-null int64 1 title 59599 non-null object 2 genres 59568 non-null object 3 authors 52714 non-null object 4 year 46720 non-null object dtypes: int64(1), object(4) memory usage: 2.3+ MB
df_items.nunique()
id 59599 title 57358 genres 10769 authors 17265 year 1053 dtype: int64
Поставлю гипотезу "Зависят ли оценка пользователя от пола?"
H0 - Различий между оценкими нет
H1 - Существуют резличия между оценками
Сформируем два образца для испытаний.
df_merge = pd.merge(df, df_users, how='inner')
men = df_merge[df_merge['sex'] == 1]
women = df_merge[df_merge['sex'] == 0]
print(f"Men mean: {men['rating'].mean():.2f}, women mean: {women['rating'].mean():.2f}")
Men mean: 4.57, women mean: 4.59
fig_1 = create_distplot([men['rating'], women['rating']],
['Men', 'Women'],
colors=['gray', 'red',],
bin_size=.2,
show_rug=False)
fig_1.update_layout(title_text='Сomparison of the distribution density in the group Men and the group Women',
template='simple_white',
font=dict(family='Arial', size=12, color='black'))
fig_1.show()
men_bootstrap = np.array([np.mean(choices(men['rating'].to_numpy(), k=2000)) for i in range(2000)])
women_bootstrap = np.array([np.mean(choices(women['rating'].to_numpy(), k=2000)) for i in range(2000)])
plt.title('Гистограмма частот для мужчин после бутстрапа')
sns.histplot(men_bootstrap)
plt.show()
plt.title('Гистограмма частот для женщин после бутстрапа')
sns.histplot(women_bootstrap)
plt.show()
alpha = 0.05
test_shap = shapiro(men_bootstrap)
print("■ Test for men")
print(f"Statistic: {test_shap.statistic:.3f}")
print('P-Value:', f'{test_shap.pvalue:.5f}')
if alpha < test_shap.pvalue:
print('Не можем отклонить H0 (данные распределены нормально)')
else:
print('Отклоняем H0 (данные распределены нормально)')
print()
test_shap = shapiro(women_bootstrap)
print("■ Test for women")
print(f"Statistic: {test_shap.statistic:.3f}")
print('P-Value:', f'{test_shap.pvalue:.5f}')
if alpha < test_shap.pvalue:
print('Не можем отклонить H0 (данные распределены нормально)')
else:
print('Отклоняем H0 (данные распределены нормально)')
■ Test for men Statistic: 0.999 P-Value: 0.25353 Не можем отклонить H0 (данные распределены нормально) ■ Test for women Statistic: 0.999 P-Value: 0.41449 Не можем отклонить H0 (данные распределены нормально)
ttest = ttest_ind(men_bootstrap, women_bootstrap, equal_var=False)
print("■ Ttest for men and women")
print(f"Statistic: {ttest.statistic:.3f}")
print('P-Value:', f'{ttest.pvalue:.5f}')
if alpha < ttest.pvalue:
print('Отклоняем H1')
else:
print('Отклоняем H0')
■ Ttest for men and women Statistic: -24.261 P-Value: 0.00000 Отклоняем H0
■ Отклоняем H0 - Различий между оценками нет. Значит разница есть. Следовательно, оценка книги зависит от пола читателя. Выбрал t-критерий Стьюдента, так как датасет достаточно большой, поэтому при бутстрапе данные распределены нормально, что необходимо для этого критерия.
print("Доверительный интервал для мужчин")
(np.mean(men_bootstrap)-1.96*np.std(men_bootstrap)/np.sqrt(1000),
np.mean(men_bootstrap)+1.96*np.std(men_bootstrap)/np.sqrt(1000))
Доверительный интервал для мужчин
(4.573505138851332, 4.575852361148669)
print("Доверительный интервал для женщин")
(np.mean(women_bootstrap)-1.96*np.std(women_bootstrap)/np.sqrt(1000),
np.mean(women_bootstrap)+1.96*np.std(women_bootstrap)/np.sqrt(1000))
Доверительный интервал для женщин
(4.588281176473816, 4.5907218235261835)
■ Построил доверительные интервалы, они не пересекаются. Следовательно, среднее значение рейтинга зависит от пола пользователя. Добавил этот пункт, чтобы убедиться в полученных результатах
fig_1 = create_distplot([men_bootstrap, women_bootstrap],
['Men', 'Women'],
colors=['gray', 'red',],
bin_size=.005,
show_rug=False)
fig_1.update_layout(title_text='Сomparison of the distribution density in the group Men and the group Women',
template='simple_white',
font=dict(family='Arial', size=12, color='black'))
fig_1.show()